SEUL Log

포트폴리오

S3 presignedUrl로 프론트에서 이미지 관리하기

June 10, 2024
aws

안녕하세요. 오늘은 aws S3, API Gateway, Lambda를 활용하여 프론트엔드에서 이미지를 직접 관리하는 방법에 대해 알아보겠습니다.


presignedUrl이 필요한 이유

프로젝트를 진행하다보면 이미지를 저장하고 불러오는 과정이 필요할 때가 있습니다. 보통의 경우 S3(Amazon Simple Storage Service)나 GCP의 Cloud storage와 같은 객체 스토리지를 사용해 이미지를 저장하게 되며 해당 과정은 아래 그림과 같이 서버에서 진행하게 됩니다.

하지만 이미지 업로드는 부하가 큰 작업으로 위와 같은 방식을 사용하게 될 경우 이미지를 저장하고 불러오는 과정이 많아질 수록 서버 부하가 커지는 문제가 발생할 수 있습니다. 그렇다면 단순히 위 과정에서 서버를 제외하고 프론트에서 직접 객체 스토리지에 이미지를 요청하고 받아오게 되면 어떨까요?

서버의 부하는 해결할 수 있겠지만 프론트에서 객체 스토리지에 이미지를 직접 요청하게 된다면 아무나 이미지를 업로드하고 삭제할 수 있다는 보안 문제가 발생할 수 있습니다.

이러한 두 가지 문제를 해결하기 위해 사용할 수 있는 것이 S3의 presignedUrl입니다. presignedUrl이란 단어에서 바로 알 수 있듯이 미리 서명된 권한을 의미하며, 즉 S3에 대한 접근 권한을 위해 임시로 발급된 URL입니다. presignedUrl을 활용하면 임시로 발급된 URL을 통해 s3에 접근할 수 있으며 이 URL은 일정 시간이 지나면 만료되어 public 접근이 불가능하도록 할 수 있습니다. 또한 설정에 따라 GET, POST, PUT, DELETE 중 하나의 요청만 허가할 수도 있습니다.

presignedUrl을 서버에서 발급받아 프론트로 전달해 프론트에서 객체 스토리지에 접근할 수도 있고 혹은 프론트에서 presignedUrl을 직접 발급받아 이미지 저장소에 접근할 수도 있습니다.

필요에 따라 어떤 방법을 사용해도 좋지만 해당 글에서는 API Gateway와 AWS Lambda를 활용해 serverless한 상태에서 객체 스토리지에 이미지를 저장하고 불러오는 방식을 알아보도록 하겠습니다. 전체 아키텍쳐는 아래 그림과 같습니다.

API Gateway와 AWS Lambda를 설정하고 간단한 함수를 불러오는 것은 해당 글에서 다뤘으므로 바로 S3와 Lambda를 활용해 presignedUrl을 발급받는 과정에 대해서 알아보겠습니다.

아래 과정 중 지역을 설정해야 하는 부분이 있다면 꼭 원하는 지역인지 확인하고 진행해주세요. ex. 서울(ap-northeast-2)

CreatePresignedPost: S3에 이미지 저장하기

IAM 생성

먼저 S3에 대한 접근 권한과 Lambda에 대한 접근 권한을 가진 IAM 사용자를 생성해야 합니다. aws console에서 IAM을 검색해 사용자를 생성해 줍니다.

직접 정책 연결을 선택하고 amazonS3FullAccessAWSLambda_FullAccess 정책을 추가해줍니다.

S3 버킷 정책 설정

기존에 만들어둔 S3 버킷이 있다고 생각하고 진행하겠습니다. 이제 S3의 객체에 대한 접근 권한을 얻기 위해 버킷 정책을 설정해야 합니다. aws 정책 생성기에 들어가서 아래와 같이 버킷 정책을 생성합니다.

권한탭에 들어가서 퍼블릭 액세스에 체크가 풀려있다면 퍼블릭 액세스를 차단해줍니다.

AWS Lambda 생성

이제 실질적으로 presignedUrl을 발급받는 코드를 lambda에서 작성해야 합니다. 마찬가지로 aws console에서 Lambda를 검색하고 함수 생성을 해줍니다. 이 때 새로 작성을 선택하고 기본 실행 역할을 기존에 생성해두었던 IAM 유저를 할당해줍니다.

먼저 S3에 이미지를 업로드하기 위한 presignedUrl을 발급받는 함수를 작성해보겠습니다. 전체 코드는 아래와 같습니다. 참고로 aws-sdk 버전별로 함수가 다르므로 주의해야 합니다.

import { S3Client } from "@aws-sdk/client-s3"; import { createPresignedPost } from "@aws-sdk/s3-presigned-post"; export const handler = async (event, context) => { const s3ClientConfiguration = { region: 'ap-northeast-2' } //client의 region const client = new S3Client(s3ClientConfiguration) const { url, fields } = await createPresignedPost(client, { Bucket: 'bucket-name', //s3 bucket 이름 Key: event.fileKey, //업로드될 파일 이름(경로 포함) Conditions: [ ], //정책을 배열로 작성 Expires: 3600 // presignedUrl의 만료시간(s) }) const response = { statusCode: 200, body: {url, fields} }; // response는 url과 fields를 포함한 원하는 형태로 작성하면 됩니다. return response; };

위 코드에서 볼 수 있듯이 createPresignedPost 함수를 통해 이미지를 post하는 url을 발급받을 수 있으며 요청시 Conditions 값을 사용해 여러 정책을 설정할 수 있습니다. 공식문서에는 Condition Mathcing시 아래와 같이 두 가지 방식을 사용할 수 있다고 설명하고 있습니다.

Exact Matches는 정확한 일치, Starts with은 시작부분이 일치하는 것을 의미하며 예를 들어 Content-Type을 설정할 경우 아래와 같이 사용할 수 있습니다.

//exact match const contentTypePolicy=['eq','$Content-Type','image/jpeg'] //or const contentTypePolicy={'Content-Type':'image/jpeg'} //starts-with const contentTypePolicy=['starts-with','$Content-type','image/']

이 외에도 content-length-range 등의 값을 설정할 수 있으며 설정할 수 있는 다양한 Conditions는 공식문서에서 확인할 수 있습니다.

API Gateway 연결

이제 API Gateway에 Lambda 함수를 연결해주겠습니다. presignedUrl을 발급하는 API는 POST 요청이므로 메서드 유형은 POST로 설정하며 미리 만들어둔 Lambda 함수와 연결해줍니다.

이렇게 만들어진 메서드를 스테이지에 배포하고 나온 url에 요청하면 presignedUrl을 발급받을 수 있습니다.

postman으로 API를 테스트 해보겠습니다. 이 때 fileKey는 ${path}/${filename} 형태로 요청하면 됩니다. postman으로 요청하면 위에서 작성한대로 url과 fields가 response로 return 됩니다.

지금까지 preSignedUlr을 발급받는 코드를 작성했습니다. 이제 발급받은 url을 사용하여 실질적으로 S3 버킷에 객체를 저장하는 코드를 작성해야 합니다. 이 때 받은 fields 값들과 이미지 파일을 formData 형태로 전송해야 하며 method는 POST이고 content-type은 multipart/form-data 형태로 전송합니다.

  const uploadImage = async ({ url, fields, file }: UploadImage) => {     const formData = new FormData()     formData.append('Content-Type', file.type)     formData.append('file', file)         for (const x in fields) {       formData.append(x, fields[x])     }     await axios.post(url, formData, {       headers: { 'Content-Type': 'multipart/form-data' },     })   },

이제 S3를 확인해보면 이미지가 잘 저장되어 있는 것을 확인할 수 있습니다.

GetSignedUrl: 저장된 이미지 가져오기

저장한 이미지를 가져오는 함수도 작성해보겠습니다. createPresignedPost와 같은 방식으로 Lambda 함수를 생성하고 API Gateway에서 메서드를 생성해 배포해줍니다. 이 때 API 메서드 또한 GET으로 메서드를 설정하는 것이 아닌 POST 요청을 사용해야 합니다.

Lambda에 작성할 내용은 아래와 같습니다.

import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3' import { getSignedUrl } from '@aws-sdk/s3-request-presigner' export const handler = async (event, context) => {   const s3ClientConfiguration = { region: 'ap-northeast-2' }   const client = new S3Client(s3ClientConfiguration)   const command = new GetObjectCommand({     Bucket: 'bucket-name', //s3 bucket 이름     Key: event.fileKey, //path + filename   })   const url = await getSignedUrl(client, command)   const response = {     statusCode: 200,     body: url,   }   return response }

포스트맨으로 요청하면 이미지 url을 받을 수 있습니다.